序言
对于 JVM 垃圾回收的工作原理,只需要理解以下三个问题:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
哪些内存需要回收?
在堆里面存放着 Java 世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。
那么,如何确定对象的存活死亡?
可达性分析算法
对于 JVM 而言,是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路就是通过 一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过 程所走过的路径称为“引用链”(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。
GC Roots
如下图所示,对象 object 5、object 6、object 7 虽然互有关联,但是它们到 GC Roots 是不可达的,因此它们将会被判定为可回收的对象。
在 Java 技术体系里面,固定可作为 GC Roots 的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如 Java 类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用
- 在本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象。
- Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized 关键字)持有的对象。
- 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
除了这些固定的 GC Roots 集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不 同,还可以有其他对象“临时性”地加入,共同构成完整 GCRoots 集合。
对象存活的引用模型
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可 达,判定对象是否存活都和“引用”离不开关系。
Java 将引用分为强引用(Strongly Re-ference)、软 引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4 种,这 4 种引用强 度依次逐渐减弱:
- 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似
Object obj=new Object()这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。 - 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 版之后提供了 SoftReference 类来实现软引用。
- 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在 JDK 1.2 版之后提供了 WeakReference 类来实现弱引用。
- 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在 JDK 1.2 版之后提供 了 PhantomReference 类来实现虚引用。
简而言之:
- 强引用:发生 gc 的时候不会被回收。
- 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
- 弱引用:有用但不是必须的对象,在下一次 GC 时会被回收。
- 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。
不可达就应该死亡嘛?
即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓 刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
- 如果对象在进行可达性分析后发现没 有与 GC Roots 相连接的引用链,那它将会被第一次标记,
- 随后进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。假如对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用 过,那么虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为确有必要执行 finalize() 方法,那么该对象将会被放置在一个名为 F-Queue 的 队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的 Finalizer 线程去执行它们的 finalize() 方法。这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。这样做的原因是,如果某个对象的 finalize() 方法执行缓慢,或者更极端地发生了死循环,将很可能导 致 F-Queue 队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。finalize() 方法是对 象逃脱死亡命运的最后一次机会,稍后收集器将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this 关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集 合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。
对象什么时候可以被垃圾回收?
当对象对当前使用这个对象的应用程序变得不可触及的时候(无可达 GC Root),这个对象就可以被回收了。
如何回收
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
- 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的
- 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡
这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
显而易见:
- 如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;
- 如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用
因此,按分代收集的理论,会根据对象的存活周期将内存划分为几块。一般包括年轻代、老年代 和 永久代,如图所示:

在 Java 堆划分出不同的区域之后,垃圾收集器可以每次只回收其中某一个或者某些部分的区域 ——因而才有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;也才能够针对不同的区域安 排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记 - 复制算法”“标记 - 清除算法”“标记-整理算法”等针对性的垃圾收集算法。
垃圾回收算法
标记 - 清除算法
标记 - 清除算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回 收所有未被标记的对象。
即标记 - 清除算法将垃圾收集分为两个阶段:
- 标记阶段:标记出可以回收的对象。
- 清除阶段:回收被标记的对象所占用的空间。
标记 - 清除算法的执行的过程如下图所示

缺点
标记 - 清除算法被认为是最基础的收集算法,因为后续的收集算法大多都是以标记 - 清除算法为基础,对其缺点进行改进而得到的。它的主要缺点有两个:
- 第一个是执行效率不稳定,如果 Java 堆中包含大量对 象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过 程的执行效率都随对象数量增长而降低;
- 第二个是内存空间的碎片化问题,标记、清除之后会产生大 量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作
复制算法
为了解决标记 - 清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。
复制算法的执行过程如下图所示

- 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片
- 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制
标记 - 整理算法
标记 - 复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100% 存 活的极端情况,所以在老年代一般不能直接选用这种算法。因此就出现了一种标记 - 整理算法(Mark-Compact)算法,与标记 - 清理算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。
标记 - 整理算法的执行过程如下图所示

- 优点:解决了标记 - 清理算法存在的内存碎片问题。
- 缺点:仍需要进行局部对象移动,一定程度上降低了效率。
垃圾收集器
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了 7 种作用于不同分代的收集器,其中用于回收新生代的收集器包括 Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括 Serial Old、Parallel Old、CMS,还有用于回收整个 Java 堆的 G1 收集器。
- 新生代回收器:Serial、ParNew、Parallel Scavenge
- 老年代回收器:Serial Old、Parallel Old、CMS
- 整堆回收器:G1
不同收集器之间的连线表示它们可以搭配使用。

- Serial 收集器(复制算法):新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
- ParNew 收集器 (复制算法):新生代收并行集器,实际上是 Serial 收集器的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现
- Parallel Scavenge 收集器 (复制算法):新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC 线程时间),高吞吐量可以高效率的利用 CPU 时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;(JDK 1.8 默认)
- Serial Old 收集器 (标记 - 整理算法):老年代单线程收集器,Serial 收集器的老年代版本;
- Parallel Old 收集器 (标记 - 整理算法):老年代并行收集器,吞吐量优先(JDK 1.8 默认)
- CMS(Concurrent Mark Sweep)收集器(标记 - 清除算法):老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短 GC 回收停顿时间。
- G1(Garbage First)收集器 (标记 - 整理算法):Java 堆并行收集器,G1 收集器是 JDK1.7 提供的一个新收集器,G1 收集器基于“标记 - 整理”算法实现,也就是说不会产生内存碎片。此外,G1 收集器不同于之前的收集器的一个重要特点是:G1 回收的范围是整个 Java 堆 (包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
CMS 垃圾回收器
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。CMS 使用的是标记 - 清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
内存分配和回收策略
对象优先在 Eden 区分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
大对象直接进入老年代
大对象指需要大量连续内存空间的Java对象(比如很长的字符串或者元素数量庞大的数组)。
大对象会直接进入老年代,这是 JVM 的一种优化策略,旨在避免将大对象放入新生代,从而减少新生代的垃圾回收频率和成本。
为什么在年轻代避免有大对象呢?
原因在于在分配空间时,它容易导致——内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复制对象时,大对象就意味着高额的内存复制开销。
HotSpot虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在 Eden 区及两个Survivor区 之间来回复制,产生大量的内存复制操作。
长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。大部分情况,对象都会首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间(s0 或者 s1)中,并将对象年龄设为 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。
对象晋升到老年代的年龄阈值可通过参数-XX:MaxTenuringThreshold设置。
扩展——动态年龄判断
为了能更好地适应不同程序的内存状况,JVM 并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到参数设置中要求的年龄。
实现原理
动态年龄判断的核心逻辑是:
- JVM 会统计 Survivor 区中相同年龄对象的总大小。
- 如果某一年龄的对象总大小超过 Survivor 区的一半(
-XX:TargetSurvivorRatio,默认50%),则所有大于或等于该年龄的对象都会被晋升到老年代,而不再等待达到MaxTenuringThreshold。
具体步骤
- 在 Young GC 时,JVM 会遍历 Survivor 区中的对象,统计每个年龄的对象大小。
- 从年龄 1 开始累加,直到某一年龄的对象总大小超过 Survivor 区的一半。
- 将该年龄作为新的晋升阈值,所有大于或等于该年龄的对象都会被晋升到老年代。
示例
假设:
- Survivor 区大小为 10MB。
TargetSurvivorRatio为 50%(即 5MB)。- 对象年龄分布如下:
- 年龄1:2MB
- 年龄2:2MB
- 年龄3:1MB
- 年龄4:1MB
那么,动态年龄判断过程如下:
- 累加年龄 1 的对象大小:2MB < 5MB,继续。
- 累加年龄 2 的对象大小:2MB + 2MB = 4MB < 5MB,继续。
- 累加年龄 3 的对象大小:4MB + 1MB = 5MB ≥ 5MB,停止。
最终结果:
- 所有年龄 ≥3 的对象(年龄 3 和年龄 4)都会被晋升到老年代。
疑问:为什么在动态年龄判断时,还累加低年龄的对象大小,不直接统计某个年龄的对象大小作为判断依据?
在JVM的动态年龄判断机制中,累加低年龄对象大小而不是直接统计某个年龄的对象大小作为判断依据,是为了更合理地平衡对象晋升的时机,避免过早或过晚晋升对象到老年代。这种设计背后有以下几个关键原因:
1. 避免过早晋升
- 问题:如果直接以某个年龄的对象大小作为判断依据,可能会导致大量低年龄对象被过早晋升到老年代。
- 原因:某些年龄的对象可能本身占比较大(例如年龄1的对象占用了Survivor区的大部分空间),但这些对象可能是短生命周期的,如果直接晋升,会导致老年代被不必要的对象填满。
- 解决方案:通过累加低年龄对象的大小,可以确保只有当多个年龄的对象总大小超过阈值时,才晋升这些对象。这样可以避免仅因为某一年龄的对象占比较大而触发晋升。
2. 平滑晋升策略
- 问题:直接以某个年龄的对象大小作为判断依据,可能会导致晋升策略过于激进或保守。
- 原因:对象的年龄分布可能是动态变化的,直接以某一年龄作为判断依据无法适应这种变化。
- 解决方案:通过累加低年龄对象的大小,可以更平滑地调整晋升策略。例如,如果年龄1和年龄2的对象总大小已经接近阈值,那么年龄3及以上的对象可以被晋升,而年龄1和年龄2的对象仍然有机会在年轻代中存活更长时间。
3. 适应对象年龄分布的多样性
- 问题:不同应用的对象年龄分布可能差异很大,直接以某一年龄作为判断依据无法适应所有场景。
- 原因:某些应用可能产生大量短生命周期对象(年龄1和年龄2占比较大),而另一些应用可能产生较多中等生命周期对象(年龄3及以上占比较大)。
- 解决方案:通过累加低年龄对象的大小,可以动态适应不同的对象年龄分布,确保晋升策略更加灵活和合理。
4. 减少老年代的压力
- 问题:如果直接以某一年龄的对象大小作为判断依据,可能会导致老年代被快速填满,从而触发Full GC。
- 原因:某些年龄的对象可能占比较大,但如果这些对象是短生命周期的,晋升到老年代后可能会很快被回收,浪费老年代空间。
- 解决方案:通过累加低年龄对象的大小,可以确保只有当多个年龄的对象总大小超过阈值时,才晋升这些对象。这样可以减少老年代的压力,避免频繁触发Full GC。
5. 示例说明
假设:
- Survivor区大小为10MB。
TargetSurvivorRatio为50%(即5MB)。- 对象年龄分布如下:
- 年龄1:3MB
- 年龄2:2MB
- 年龄3:1MB
- 年龄4:1MB
直接以某一年龄作为判断依据:
- 如果以年龄1的对象大小(3MB)作为判断依据,可能会认为年龄1的对象占比较大,从而晋升年龄1及以上的对象。
- 但这会导致大量短生命周期对象被过早晋升到老年代,浪费老年代空间。
累加低年龄对象大小:
- 累加年龄1的对象大小:3MB < 5MB,继续。
- 累加年龄2的对象大小:3MB + 2MB = 5MB ≥ 5MB,停止。
- 结果:所有年龄≥2的对象(年龄2、年龄3、年龄4)都会被晋升到老年代。
通过累加低年龄对象大小,可以更合理地判断晋升时机,避免过早晋升短生命周期对象。
总结
动态年龄判断机制通过累加低年龄对象的大小,而不是直接以某一年龄的对象大小作为判断依据,能够:
- 避免过早晋升短生命周期对象。
- 平滑调整晋升策略,适应不同的对象年龄分布。
- 减少老年代的压力,降低Full GC的频率。
这种设计使得JVM能够更灵活地管理对象生命周期,优化垃圾回收性能。
空间分配担保
为了确保在 Minor GC 之前老年代本身还有容纳新生代所有晋升对象的剩余空间。空间分配担保机制要求在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有晋升对象总空间:
- 如果这个条件成立,那这一次 Minor GC 可以确保是安全的,直接进行 Young GC
- 如果不成立,则虚拟机会先查看
XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);- 如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小
- 如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;
- 如果小于,或者
-XX: HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次 Full GC
- 如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小
解释一下“冒险”是冒了什么风险:前面提到过,新生代使用复制收集算法,但为了内存利用率, 只使用其中一个 Survivor 空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况 ——最极端的情况就是内存回收后新生代中所有对象都存活,需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代,这与生活中贷款担保类似。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,但一共有多少对象会在这次回收中活下来在实际完成内存回收之前是无法明确知道的,所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与老年代的剩余空间进行比较,决定是否进行 Full GC 来让老年代腾出更多空间。
取历史平均值来比较其实仍然是一种赌概率的解决办法,也就是说假如某次 Minor GC 存活后的对象突增,远远高于历史平均值的话,依然会导致担保失败。如果出现了担保失败,那就只好老老实实地重新发起一次Full GC,这样停顿时间就很长了。虽然担保失败时绕的圈子是最大的,但通常情况下都还是会将-XX:HandlePromotionFailure开关打开,避免Full GC过于频繁。
分代垃圾回收器工作原理
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
- 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
- 清空 Eden 和 From Survivor 分区;
- From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级到老年代。大对象也会直接进入老生代。老生代的空间占用到达某个值之后就会触发 Full GC。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
各种 GC 的区别
垃圾回收方式又可以分为不同的阶段,其中包括 Minor GC(Young GC)、Major GC(Old GC)、和 Full GC 几种不同的垃圾回收阶段:
- Minor GC(或 Young GC):它是针对新生代(Young Generation)进行的垃圾回收。在 Young GC 中,虚拟机会对 Eden 区和 Survivor 区进行垃圾回收,将存活的对象复制到另一个 Survivor 区中,并清空当前 Survivor 区。如果一个对象已经经历了多次 Minor GC 仍然存活,那么它会被复制到老年代(Old Generation)中
- Major GC(或 Old GC):它是针对老年代进行的垃圾回收。在 Major GC 中,虚拟机会对老年代进行垃圾回收。(目前只有 CMS 收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,读者需按上下文区分到底是指老年代的收集还是整堆收集。)
- Full GC:它是对整个堆进行垃圾回收,包括新生代、老年代和方法区(元空间)。Full GC 通常由 Minor GC 触发,但是它的耗时比较长,因为它需要对整个堆进行垃圾回收
Young GC 和 Full GC 的区别:Young GC 和 Full GC 的主要区别在于垃圾回收的范围。Young GC 只回收新生代的对象,而 Full GC 则回收整个堆中的对象。因为新生代中的对象通常比较少,所以 Young GC 的耗时比较短,而 Full GC 的耗时则比较长。
疑问
如何判断一个对象应该被回收?
- 该对象没有与 GC Roots 相连
- 该对象没有重写 finalize() 方法或 finalize() 已经被执行过则直接回收(第一次标记),否则将对象加入到 F-Queue 队列中(优先级很低的队列)在这里 finalize() 方法被执行,之后进行第二次标记时,如果对象仍然应该被 GC 则 GC,否则移除队列。(在 finalize 方法中,对象很可能和其他 GC Roots 中的某一个对象建立了关联,finalize 方法只会被调用一次,且不推荐使用 finalize 方法)
为什么 Xmx 和 Xms 参数建议设置为相同的大小?
在 Java 应用程序中,JVM 的 Xmx 参数表示 JVM 最大可用的堆内存大小,Xms 参数表示 JVM 初始分配的堆内存大小。将 Xmx 和 Xms 设置为相同的大小,可以减少 JVM 运行时动态分配内存的次数,从而提高应用程序的性能和稳定性。
如果 Xmx 和 Xms 参数设置为不同的大小,那么 JVM 在启动时会先分配 Xms 指定大小的堆内存,然后在应用程序运行时根据需要动态调整堆内存大小,直到达到 Xmx 指定的最大内存大小。这种动态分配内存的方式会导致 JVM 运行时频繁进行垃圾回收和内存整理,从而影响应用程序的性能和稳定性。
将 Xmx 和 Xms 设置为相同的大小可以避免这种动态分配内存的方式,从而减少垃圾回收和内存整理的次数,提高应用程序的性能和稳定性。通常建议将 Xmx 和 Xms 设置为相同的大小,并根据应用程序的实际内存需求进行适当的调整,以达到最佳的性能和稳定性。
YoungGC 和 FullGC 大概什么时候会被触发?
Young GC 和 Full GC 的触发时机和条件不同。
Young GC 的触发时机
Young GC 是针对新生代进行的垃圾回收,在新生代中存活时间较短的对象会被回收掉,因此 Young GC 的触发时机通常与新生代的内存使用情况有关。当新生代中的对象占用的内存达到一定的阈值时,就会触发 Young GC。
具体触发 Young GC 的条件包括:
- Eden 区满了
- 一个 Survivor 区已满
- Survivor 对象年龄达到阈值
Full GC 的触发时机
Full GC 是对整个堆进行的垃圾回收,因此它的触发时机通常与整个堆的内存使用情况有关。
Full GC 的触发条件比较复杂,包括以下情况:
System.gc()方法显式调用- 元空间内存空间不足,即其内存达到了所设定的阈值
-XX:MetaspaceSize=128M - 老年代内存空间不足,空间分配担保失败:
- 检查发现内存不足:发生 Young GC 之前进行检查,如果”老年代可用的连续内存空间” < “新生代历次 Young GC 后升入老年代的对象总和的平均大小”,说明本次 Young GC 后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间,此时会触发 Full GC
- 执行发现内存不足:执行 Young GC 后,有一批对象需要放入老年代,但此时老年代无足够内存空间存放这些对象,此时必须立即触发一次 Full GC
- G1 GC 的 Mixed GC
Full GC 比较频繁,可能是什么原因?
当 Full GC(Full Garbage Collection,全局垃圾回收)频繁发生时,可能出现以下几个常见的原因:
- 内存泄漏:内存泄漏是指无用对象无法被垃圾回收器正确回收,导致内存占用不断增加。如果存在大量的内存泄漏,垃圾回收器可能需要执行频繁的 Full GC 来回收无用对象,以避免内存溢出。可以通过内存分析工具来检测和定位内存泄漏问题,并进行修复
- 内存分配过小:如果应用程序的内存分配过小,无法满足对象的存储需求,垃圾回收器可能会频繁触发 Full GC 来尝试释放内存。这可能是因为应用程序的内存设置不合理,可以通过增加分配给应用程序的内存大小来缓解这个问题
- 过多的对象存活时间:如果应用程序中存在大量长时间存活的对象,垃圾回收器可能需要执行更频繁的 Full GC 来回收这些对象。这可能是因为应用程序的设计或算法导致对象的生命周期较长,可以通过优化代码逻辑或引入对象池等技术来减少对象的存活时间
- GC 调优参数设置不合理:Java 虚拟机的垃圾回收调优参数对 Full GC 的触发频率有影响。如果参数设置不合理,垃圾回收器可能会过早或过于频繁地执行 Full GC。可以通过调整垃圾回收相关的参数(如堆大小、新生代和老年代的比例、垃圾回收算法等)来优化垃圾回收性能
- 并发处理引起的长时间停顿:在进行 Full GC 时,垃圾回收器会暂停应用程序的执行。如果 Full GC 需要处理的垃圾回收任务较大,导致停顿时间过长,应用程序可能会出现明显的性能问题。可以考虑调整垃圾回收器的参数,选择合适的垃圾回收器或使用并发垃圾回收器(如 CMS、G1 等),以减少停顿时间
老年代满了并且其中的对象无法回收,那么 Young GC 的老对象会分配到哪里去?
在 Java 的垃圾回收机制中,若老年代已满并且其中的对象无法回收,那么发生 Full GC 时会尝试对整个堆进行垃圾回收,包括年轻代(Young Generation)和老年代。
具体来说,如果发生了 Full GC 而老年代中有对象无法回收,它们可能会被标记为垃圾(如果符合垃圾回收条件)。如果老年代没有足够的空间来容纳所有活跃的老对象,JVM 会尝试以下几种策略:
- 直接扩展老年代空间:如果系统允许,JVM 可能会扩大老年代的大小,但这通常是在系统资源允许的情况下进行的,并且可能会影响性能。
- Young GC(Minor GC)后将对象晋升到老年代:在进行 Young GC(Minor GC)时,如果有足够空间,晋升到老年代的对象会按照通常的晋升逻辑进行分配。如果老年代空间不足以容纳新的晋升对象,JVM 会进行 Full GC,尝试清理老年代空间。
- “Promotion Failure” 机制:如果老年代的空间不足以容纳所有晋升的对象,JVM 可能会抛出
OutOfMemoryError,提示老年代内存不足。
所以,简而言之,老年代空间满且对象无法回收时,系统会尽力通过 Full GC 回收空间,若回收不够,可能会导致 OutOfMemoryError 错误。
垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
对于 GC 来说,当程序员创建对象时,GC 就开始监控这个对象的地址、大小以及使用情况。通常,GC 采用有向图的方式记录和管理堆 (heap) 中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当 GC 确定一些对象为”不可达”时,GC 就有责任回收这些内存空间。程序员可以手动执行 System.gc(),通知 GC 运行,但是 Java 语言规范并不保证 GC 一定会执行。
JVM 中的永久代中会发生垃圾回收吗?
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收 (Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免 Full GC 是非常重要的原因。
参考资料
- 周志明.深入理解 Java 虚拟机 [M]. 机械工业出版社,2013
文章信息
| 时间 | 说明 |
|---|---|
| 2025-01-05 | 初稿 |